En grundig gjennomgang av Reacts useSyncExternalStore-hook for synkronisering av eksterne datakilder, inkludert implementeringsstrategier og ytelseshensyn.
React useSyncExternalStore: Mestring av synkronisering med eksterne datakilder
I moderne React-applikasjoner er effektiv tilstandshåndtering avgjørende. Mens React tilbyr innebygde løsninger for tilstandshåndtering som useState og useReducer, krever integrasjon med eksterne datakilder eller tredjepartsbiblioteker for tilstandshåndtering en mer sofistikert tilnærming. Det er her useSyncExternalStore kommer inn i bildet.
Hva er useSyncExternalStore?
useSyncExternalStore er en React-hook introdusert i React 18 som lar deg abonnere på og lese fra eksterne datakilder på en måte som er kompatibel med samtidig (concurrent) rendring. Dette er spesielt viktig når man håndterer data som ikke administreres direkte av React, som for eksempel:
- Tredjepartsbiblioteker for tilstandshåndtering: Redux, Zustand, Jotai, osv.
- Nettleser-API-er:
localStorage,IndexedDB, osv. - Eksterne datakilder: Server-sent events, WebSockets, osv.
Før useSyncExternalStore kunne synkronisering av eksterne stores føre til «tearing» og inkonsistens, spesielt med Reacts funksjoner for samtidig rendring. Denne hooken løser disse problemene ved å tilby en standardisert og ytelseseffektiv måte å koble eksterne data til dine React-komponenter.
Hvorfor bruke useSyncExternalStore? Fordeler og gevinster
Å bruke useSyncExternalStore tilbyr flere sentrale fordeler:
- Sikkerhet ved samtidig rendring (Concurrency Safety): Sikrer at komponenten din alltid viser en konsistent visning av den eksterne storen, selv under samtidige rendringer. Dette forhindrer «tearing»-problemer der deler av brukergrensesnittet kan vise inkonsistente data.
- Ytelse: Optimalisert for ytelse, og minimerer unødvendige re-rendringer. Den utnytter Reacts interne mekanismer for å effektivt abonnere på endringer og oppdatere komponenten kun når det er nødvendig.
- Standardisert API: Tilbyr et konsistent og forutsigbart API for å samhandle med eksterne stores, uavhengig av den underliggende implementasjonen.
- Redusert standardkode (Boilerplate): Forenkler prosessen med å koble til eksterne stores, og reduserer mengden egendefinert kode du må skrive.
- Kompatibilitet: Fungerer sømløst med et bredt spekter av eksterne datakilder og biblioteker for tilstandshåndtering.
Hvordan useSyncExternalStore fungerer: En dybdegående titt
useSyncExternalStore-hooken tar tre argumenter:
subscribe(callback: () => void): () => void: En funksjon som registrerer en callback for å bli varslet når den eksterne storen endres. Den skal returnere en funksjon for å avslutte abonnementet. Slik lærer React når storen har nye data.getSnapshot(): T: En funksjon som returnerer et øyeblikksbilde (snapshot) av dataene fra den eksterne storen. Dette øyeblikksbildet bør være en enkel, uforanderlig verdi som React kan bruke for å avgjøre om dataene har endret seg.getServerSnapshot?(): T(Valgfritt): En funksjon som returnerer det opprinnelige øyeblikksbildet av dataene på serveren. Dette brukes for server-side rendering (SSR) for å sikre konsistens mellom server og klient. Hvis den ikke er oppgitt, vil React brukegetSnapshot()under server-rendring, noe som kanskje ikke er ideelt for alle scenarier.
Her er en oversikt over hvordan disse argumentene fungerer sammen:
- Når komponenten monteres, kaller
useSyncExternalStoresubscribe-funksjonen for å registrere en callback. - Når den eksterne storen endres, kaller den callbacken som ble registrert gjennom
subscribe. - Callbacken forteller React at komponenten må re-rendres.
- Under rendringen kaller
useSyncExternalStoregetSnapshotfor å hente de nyeste dataene fra den eksterne storen. - React sammenligner det nåværende øyeblikksbildet med det forrige. Hvis de er forskjellige, oppdateres komponenten med de nye dataene.
- Når komponenten avmonteres, kalles funksjonen for å avslutte abonnementet, som ble returnert av
subscribe, for å forhindre minnelekkasjer.
Grunnleggende implementasjonseksempel: Integrasjon med localStorage
La oss illustrere hvordan man bruker useSyncExternalStore med et enkelt eksempel: å lese og skrive en verdi til localStorage.
import { useSyncExternalStore } from 'react';
function getLocalStorageItem(key: string): string | null {
try {
return localStorage.getItem(key);
} catch (error) {
console.error("Error accessing localStorage:", error);
return null; // Håndter potensielle feil, som at `localStorage` er utilgjengelig.
}
}
function useLocalStorage(key: string): [string | null, (value: string) => void] {
const subscribe = (callback: () => void) => {
window.addEventListener('storage', callback);
return () => window.removeEventListener('storage', callback);
};
const getSnapshot = () => getLocalStorageItem(key);
const serverSnapshot = () => null; // Eller en standardverdi hvis det passer for ditt SSR-oppsett
const value = useSyncExternalStore(subscribe, getSnapshot, serverSnapshot);
const setValue = (newValue: string) => {
try {
localStorage.setItem(key, newValue);
// Send en 'storage'-hendelse på det nåværende vinduet for å utløse oppdateringer i andre faner.
window.dispatchEvent(new StorageEvent('storage', {
key: key,
newValue: newValue,
storageArea: localStorage,
} as StorageEventInit));
} catch (error) {
console.error("Error setting localStorage:", error);
}
};
return [value, setValue];
}
function MyComponent() {
const [name, setName] = useLocalStorage('name');
return (
<div>
<p>Hello, {name || 'World'}</p>
<input
type="text"
value={name || ''}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
}
export default MyComponent;
Forklaring:
getLocalStorageItem: En hjelpefunksjon for å trygt hente verdien fralocalStorage, og håndtere potensielle feil.useLocalStorage: En egendefinert hook som innkapsler logikken for å samhandle medlocalStorageved hjelp avuseSyncExternalStore.subscribe: Lytter etter'storage'-hendelsen, som utløses nårlocalStorageendres i en annen fane eller et annet vindu. Kritisk nok sender vi en 'storage'-hendelse etter å ha satt en ny verdi for å korrekt utløse oppdateringer i det *samme* vinduet.getSnapshot: Returnerer den nåværende verdien fralocalStorage.serverSnapshot: Returnerernull(eller en standardverdi) for server-side rendering.setValue: Oppdaterer verdien ilocalStorageog sender en 'storage'-hendelse for å signalisere til andre faner.MyComponent: En enkel komponent som brukeruseLocalStorage-hooken for å vise og oppdatere et navn.
Viktige hensyn for localStorage:
- Feilhåndtering: Omslutt alltid tilgang til
localStorageitry...catch-blokker for å håndtere potensielle feil, for eksempel nårlocalStorageer deaktivert eller utilgjengelig (f.eks. i privat nettlesermodus). - Storage-hendelser:
'storage'-hendelsen utløses kun nårlocalStorageendres i en *annen* fane eller et annet vindu, ikke i det samme vinduet. Derfor sender vi en nyStorageEventmanuelt etter å ha satt en verdi. - Dataserialisering:
localStoragelagrer kun strenger. Du kan måtte serialisere og deserialisere komplekse datastrukturer ved hjelp avJSON.stringifyogJSON.parse. - Sikkerhet: Vær oppmerksom på dataene du lagrer i
localStorage, da de er tilgjengelige for JavaScript-kode på samme domene. Sensitiv informasjon bør ikke lagres ilocalStorage.
Avanserte bruksområder og eksempler
1. Integrasjon med Zustand (eller andre biblioteker for tilstandshåndtering)
Å integrere useSyncExternalStore med et globalt bibliotek for tilstandshåndtering som Zustand er et vanlig bruksområde. Her er et eksempel:
import { useSyncExternalStore } from 'react';
import { create } from 'zustand';
interface BearState {
bears: number
increase: (by: number) => void
}
const useStore = create<BearState>((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by }))
}))
function BearCounter() {
const bears = useSyncExternalStore(
useStore.subscribe,
useStore.getState,
() => ({ bears: 0, increase: () => {} }) // Server-snapshot, oppgi standardtilstand
).bears
return <h1>{bears} bears around here!</h1>
}
function Controls() {
const increase = useStore(state => state.increase)
return (<button onClick={() => increase(1)}>one bear</button>)
}
export { BearCounter, Controls }
Forklaring:
- Vi bruker Zustand for global tilstandshåndtering
useStore.subscribe: Denne funksjonen abonnerer på Zustand-storen og vil utløse re-rendringer når storens tilstand endres.useStore.getState: Denne funksjonen returnerer den nåværende tilstanden til Zustand-storen.- Det tredje parameteret gir en standardtilstand for server-side rendering (SSR), noe som sikrer at komponenten rendres korrekt på serveren før JavaScript på klientsiden tar over.
- Komponenten henter antall bjørner ved hjelp av
useSyncExternalStoreog rendrer det. Controls-komponenten viser hvordan man bruker en Zustand-setter.
2. Integrasjon med Server-Sent Events (SSE)
useSyncExternalStore kan brukes for å effektivt oppdatere komponenter basert på sanntidsdata fra en server ved hjelp av Server-Sent Events (SSE).
import { useSyncExternalStore, useState, useEffect, useCallback } from 'react';
function useSSE(url: string) {
const [data, setData] = useState(null);
const [eventSource, setEventSource] = useState(null);
useEffect(() => {
const newEventSource = new EventSource(url);
setEventSource(newEventSource);
newEventSource.onmessage = (event) => {
try {
const parsedData = JSON.parse(event.data);
setData(parsedData);
} catch (error) {
console.error("Error parsing SSE data:", error);
}
};
newEventSource.onerror = (error) => {
console.error("SSE error:", error);
};
return () => {
newEventSource.close();
setEventSource(null);
};
}, [url]);
const subscribe = useCallback((callback: () => void) => {
if (eventSource) {
eventSource.addEventListener('message', callback);
}
return () => {
if (eventSource) {
eventSource.removeEventListener('message', callback);
}
};
}, [eventSource]);
const getSnapshot = useCallback(() => data, [data]);
const serverSnapshot = useCallback(() => null, []);
const value = useSyncExternalStore(subscribe, getSnapshot, serverSnapshot);
return value;
}
function RealTimeDataComponent() {
const realTimeData = useSSE('/api/sse'); // Erstatt med ditt SSE-endepunkt
if (!realTimeData) {
return <p>Loading...</p>;
}
return <div><p>Real-time Data: {JSON.stringify(realTimeData)}</p></div>;
}
export default RealTimeDataComponent;
Forklaring:
useSSE: En egendefinert hook som etablerer en SSE-tilkobling til en gitt URL.subscribe: Legger til en hendelseslytter påEventSource-objektet for å bli varslet om nye meldinger fra serveren. Den brukeruseCallbackfor å sikre at callback-funksjonen ikke opprettes på nytt ved hver rendring.getSnapshot: Returnerer de sist mottatte dataene fra SSE-strømmen.serverSnapshot: Returnerernullfor server-side rendering.RealTimeDataComponent: En komponent som brukeruseSSE-hooken for å vise sanntidsdata.
3. Integrasjon med IndexedDB
Synkroniser React-komponenter med data lagret i IndexedDB ved hjelp av useSyncExternalStore.
import { useSyncExternalStore, useState, useEffect, useCallback } from 'react';
interface IDBData {
id: number;
name: string;
}
async function getAllData(): Promise {
return new Promise((resolve, reject) => {
const request = indexedDB.open('myDataBase', 1); // Erstatt med ditt databasenavn og versjon
request.onerror = (event) => {
console.error("IndexedDB open error:", event);
reject(event);
};
request.onsuccess = (event) => {
const db = (event.target as IDBRequest).result as IDBDatabase;
const transaction = db.transaction(['myDataStore'], 'readonly'); // Erstatt med ditt store-navn
const objectStore = transaction.objectStore('myDataStore');
const getAllRequest = objectStore.getAll();
getAllRequest.onsuccess = (event) => {
const data = (event.target as IDBRequest).result as IDBData[];
resolve(data);
};
getAllRequest.onerror = (event) => {
console.error("IndexedDB getAll error:", event);
reject(event);
};
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBRequest).result as IDBDatabase;
db.createObjectStore('myDataStore', { keyPath: 'id' });
};
});
}
function useIndexedDBData(): IDBData[] | null {
const [data, setData] = useState(null);
const [dbInitialized, setDbInitialized] = useState(false);
useEffect(() => {
const initDB = async () => {
try{
await getAllData();
setDbInitialized(true);
} catch (e) {
console.error("IndexedDB initialization failed", e);
}
}
initDB();
}, []);
const subscribe = useCallback((callback: () => void) => {
// Bruk debounce på callbacken for å forhindre for mange re-rendringer.
let timeoutId: NodeJS.Timeout;
const debouncedCallback = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(callback, 50); // Juster debounce-forsinkelsen etter behov
};
const handleVisibilityChange = () => {
// Hent data på nytt når fanen blir synlig igjen
if (document.visibilityState === 'visible') {
debouncedCallback();
}
};
window.addEventListener('focus', debouncedCallback);
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
window.removeEventListener('focus', debouncedCallback);
document.removeEventListener('visibilitychange', handleVisibilityChange);
clearTimeout(timeoutId);
};
}, []);
const getSnapshot = useCallback(() => {
// Hent de nyeste dataene fra IndexedDB hver gang getSnapshot kalles
getAllData().then(newData => setData(newData));
return data;
}, [data]);
const serverSnapshot = useCallback(() => null, []);
return useSyncExternalStore(subscribe, getSnapshot, serverSnapshot);
}
function IndexedDBComponent() {
const data = useIndexedDBData();
if (!data) {
return <p>Loading data from IndexedDB...</p>;
}
return (
<div>
<h2>Data from IndexedDB:</h2>
<ul>
{data.map((item) => (
<li key={item.id}>{item.name} (ID: {item.id})</li>
))}
</ul>
</div>
);
}
export default IndexedDBComponent;
Forklaring:
getAllData: En asynkron funksjon som henter alle data fra IndexedDB-storen.useIndexedDBData: En egendefinert hook som brukeruseSyncExternalStorefor å abonnere på endringer i IndexedDB.subscribe: Setter opp lyttere for synlighets- og fokusendringer for å oppdatere dataene fra IndexedDB og bruker en debounce-funksjon for å unngå for mange oppdateringer.getSnapshot: Henter det nåværende øyeblikksbildet ved å kalle `getAllData()` og returnerer deretter `data` fra tilstanden.serverSnapshot: Returnerernullfor server-side rendering.IndexedDBComponent: En komponent som viser dataene fra IndexedDB.
Viktige hensyn for IndexedDB:
- Asynkrone operasjoner: Interaksjoner med IndexedDB er asynkrone, så du må håndtere den asynkrone naturen til datahenting og -oppdateringer nøye.
- Feilhåndtering: Implementer robust feilhåndtering for å elegant håndtere potensielle problemer med databasetilgang, som at databasen ikke blir funnet eller tillatelsesfeil.
- Databaseversjonering: Håndter databaseversjoner nøye ved hjelp av
onupgradeneeded-hendelsen for å sikre datakompatibilitet etter hvert som applikasjonen din utvikler seg. - Ytelse: IndexedDB-operasjoner kan være relativt trege, spesielt for store datasett. Optimaliser spørringer og indeksering for å forbedre ytelsen.
Ytelseshensyn
Selv om useSyncExternalStore er optimalisert for ytelse, er det fortsatt noen hensyn å ta:
- Minimer endringer i øyeblikksbildet: Sørg for at
getSnapshot-funksjonen kun returnerer et nytt øyeblikksbilde når dataene faktisk har endret seg. Unngå å opprette nye objekter eller arrays unødvendig. Vurder å bruke memoization-teknikker for å optimalisere opprettelsen av øyeblikksbilder. - Batch-oppdateringer: Hvis mulig, grupper oppdateringer til den eksterne storen for å redusere antall re-rendringer. For eksempel, hvis du oppdaterer flere egenskaper i storen, prøv å oppdatere dem alle i en enkelt transaksjon.
- Debouncing/Throttling: Hvis den eksterne storen endres hyppig, bør du vurdere å bruke debouncing eller throttling på oppdateringene til React-komponenten. Dette kan forhindre for mange re-rendringer og forbedre ytelsen. Dette er spesielt nyttig med flyktige stores som endringer i nettleservinduets størrelse.
- Overfladisk sammenligning (Shallow Comparison): Sørg for at du returnerer primitive verdier eller uforanderlige objekter i
getSnapshotslik at React raskt kan avgjøre om dataene har endret seg ved hjelp av en overfladisk sammenligning. - Betingede oppdateringer: I tilfeller der den eksterne storen endres hyppig, men komponenten din bare trenger å reagere på visse endringer, bør du vurdere å implementere betingede oppdateringer i `subscribe`-funksjonen for å unngå unødvendige re-rendringer.
Vanlige fallgruver og feilsøking
- «Tearing»-problemer: Hvis du fortsatt opplever «tearing»-problemer etter å ha tatt i bruk
useSyncExternalStore, dobbeltsjekk atgetSnapshot-funksjonen din returnerer en konsistent visning av dataene, og atsubscribe-funksjonen korrekt varsler React om endringer. Sørg for at du ikke muterer dataene direkte igetSnapshot-funksjonen. - Uendelige løkker: En uendelig løkke kan oppstå hvis
getSnapshot-funksjonen alltid returnerer en ny verdi, selv når dataene ikke har endret seg. Dette kan skje hvis du oppretter nye objekter eller arrays unødvendig. Sørg for at du returnerer den samme verdien hvis dataene ikke har endret seg. - Mangler server-side rendering: Hvis du bruker server-side rendering, må du sørge for å oppgi en
getServerSnapshot-funksjon for å sikre at komponenten rendres korrekt på serveren. Denne funksjonen bør returnere den opprinnelige tilstanden til den eksterne storen. - Feilaktig avslutning av abonnement: Sørg alltid for at du korrekt avslutter abonnementet på den eksterne storen i funksjonen som returneres av
subscribe. Unnlatelse av å gjøre dette kan føre til minnelekkasjer. - Feil bruk med Concurrent Mode: Sørg for at din eksterne store er kompatibel med Concurrent Mode. Unngå å gjøre mutasjoner i den eksterne storen mens React rendrer. Mutasjoner bør være synkrone og forutsigbare.
Konklusjon
useSyncExternalStore er et kraftig verktøy for å synkronisere React-komponenter med eksterne datakilder. Ved å forstå hvordan den fungerer og følge beste praksis, kan du sikre at komponentene dine viser konsistente og oppdaterte data, selv i komplekse scenarier med samtidig rendring. Denne hooken forenkler integrasjonen med ulike datakilder, fra tredjepartsbiblioteker for tilstandshåndtering til nettleser-API-er og sanntids datastrømmer, noe som fører til mer robuste og ytelseseffektive React-applikasjoner. Husk å alltid håndtere potensielle feil, optimalisere ytelsen og nøye administrere abonnementer for å unngå vanlige fallgruver.